QuickOPC User's Guide and Reference
Installed Examples - Console - UAConsoleLiveMapping

Creates an object structure for a boiler, describes its mapping into OPC Unified Architecture server using attributes, and then performs the live mapping. Boiler data is then read, written and/or subscribed to using plain .NET object access..

The main program:

// UAConsoleLiveMapping: Creates an object structure for a boiler, describes its mapping into OPC Unified Architecture server 
// using attributes, and then performs the live mapping. Boiler data is then read, written and/or subscribed to using plain .NET 
// object access.

using System;
using System.Diagnostics;
using System.Threading;
using OpcLabs.EasyOpc.UA;
using OpcLabs.EasyOpc.UA.LiveMapping;
using OpcLabs.EasyOpc.UA.LiveMapping.Extensions;
using OpcLabs.EasyOpc.UA.Navigation;
using OpcLabs.EasyOpc.UA.OperationModel;

namespace UAConsoleLiveMapping
{
    class Program
    {
        static void Main()
        {
            // the OPC server
            UAEndpointDescriptor endpointDescriptor =
                "opc.tcp://opcua.demo-this.com:51210/UA/SampleServer";
            // or "http://opcua.demo-this.com:51211/UA/SampleServer" (currently not supported)
            // or "https://opcua.demo-this.com:51212/UA/SampleServer/"
            
            Console.WriteLine();
            Console.WriteLine("Mapping our data structures to OPC...");
            var mapper = new UAClientMapper();
            var boiler1 = new Boiler();
            mapper.Map(boiler1, new UAMappingContext
            {
                EndpointDescriptor = endpointDescriptor,   
                // The NodeDescriptor below determines where in the OPC address space we want to map our data to.
                NodeDescriptor = new UANodeDescriptor
                    {
                        // '#' is a reserved character in a browse name, and must be escaped by '&' in the path below.
                        BrowsePath = UABrowsePath.Parse("[ObjectsFolder]/Boilers/Boiler &#1", "http://opcfoundation.org/UA/Boiler/")
                    },
                MonitoringParameters = 1000,  // requested sampling interval (for subscriptions)
            });

            Console.WriteLine();
            Console.WriteLine("Starting the simulation of the boiler in the server, using an OPC method call...");
            // Currently there is no live mapping for OPC methods, therefore we call the OPC method in a traditional way.
            try
            {
                EasyUAClient.SharedInstance.CallMethod(
                    endpointDescriptor,
                    UABrowsePath.Parse("[ObjectsFolder]/Boilers/Boiler &#1/Simulation", "http://opcfoundation.org/UA/Boiler/"),
                    UABrowsePath.Parse("[nsu=http://opcfoundation.org/UA/Boiler/;i=1287].Start", "http://opcfoundation.org/UA/"));
            }
            catch (UAException)
            {
                // Production code would test the current state of the simulation first, and also handle the exception here.
            }

            Console.WriteLine();
            Console.WriteLine("Reading all data of the boiler...");
            mapper.Read();
            Console.WriteLine($"Drum level is: {boiler1.Drum.LevelIndicator.Output}");

            Console.WriteLine();
            Console.WriteLine("Writing new setpoint value...");
            boiler1.LevelController.SetPoint = 50.0;
            Debug.Assert(!(boiler1.LevelController is null));
            mapper.WriteTarget(boiler1.LevelController, /*recurse:*/false);

            Console.WriteLine();
            Console.WriteLine("Subscribing to boiler data changes...");
            mapper.Subscribe(/*active:*/true);

            Thread.Sleep(30 * 1000);

            Console.WriteLine();
            Console.WriteLine("Unsubscribing from boiler data changes...");
            mapper.Subscribe(/*active:*/false);

            Console.WriteLine();
            Console.WriteLine("Press Enter to continue...");
            Console.ReadLine();
        }
    }
}
' UAConsoleLiveMapping: Creates an object structure for a boiler, describes its mapping into OPC Unified Architecture server 
' using attributes, and then performs the live mapping. Boiler data is then read, written and/or subscribed to using plain .NET 
' object access.
Imports System.Threading
Imports OpcLabs.EasyOpc.UA
Imports OpcLabs.EasyOpc.UA.LiveMapping
Imports OpcLabs.EasyOpc.UA.LiveMapping.Extensions
Imports OpcLabs.EasyOpc.UA.Navigation
Imports OpcLabs.EasyOpc.UA.OperationModel

Friend Class Program
    Shared Sub Main()
        ' Define which server we will work with.
        Dim endpointDescriptor As UAEndpointDescriptor =
                "opc.tcp://opcua.demo-this.com:51210/UA/SampleServer"
        ' or "http://opcua.demo-this.com:51211/UA/SampleServer" (currently not supported)
        ' or "https://opcua.demo-this.com:51212/UA/SampleServer/"

        Console.WriteLine()
        Console.WriteLine("Mapping our data structures to OPC...")
        Dim mapper = New UAClientMapper()
        Dim boiler1 = New Boiler()
        mapper.Map(boiler1, New UAMappingContext With { _
                      .EndpointDescriptor = endpointDescriptor, _
                      .NodeDescriptor = New UANodeDescriptor With { _
                      .BrowsePath = UABrowsePath.Parse("[ObjectsFolder]/Boilers/Boiler #1", "http://opcfoundation.org/UA/Boiler/")}, _
                      .MonitoringParameters = 1000}) ' requested sampling interval (for subscriptions) -  local OPC server
        ' The NodeDescriptor below determines where in the OPC address space we want to map our data to.

        Console.WriteLine()
        Console.WriteLine("Starting the simulation of the boiler in the server, using an OPC method call...")
        ' Currently there is no live mapping for OPC methods, therefore we call the OPC method in a traditional way.
        Try
            EasyUAClient.SharedInstance.CallMethod( _
                endpointDescriptor, _
                UABrowsePath.Parse("[ObjectsFolder]/Boilers/Boiler #1/Simulation", "http://opcfoundation.org/UA/Boiler/"), _
                UABrowsePath.Parse("[nsu=http://opcfoundation.org/UA/Boiler/;i=1287].Start", "http://opcfoundation.org/UA/"))
        Catch e1 As UAException
            ' Production code would test the current state of the simulation first, and also handle the exception here.
        End Try

        Console.WriteLine()
        Console.WriteLine("Reading all data of the boiler...")
        mapper.Read()
        Console.WriteLine("Drum level is: {0}", boiler1.Drum.LevelIndicator.Output)

        Console.WriteLine()
        Console.WriteLine("Writing new setpoint value...")
        boiler1.LevelController.SetPoint = 50.0
        Debug.Assert(boiler1.LevelController IsNot Nothing)
        mapper.WriteTarget(boiler1.LevelController, False) 'recurse:

        Console.WriteLine()
        Console.WriteLine("Subscribing to boiler data changes...")
        mapper.Subscribe(True) 'active:

        Thread.Sleep(30 * 1000)

        Console.WriteLine()
        Console.WriteLine("Unsubscribing from boiler data changes...")
        mapper.Subscribe(False) 'active:

        Console.WriteLine()
        Console.WriteLine("Press Enter to continue...")
        Console.ReadLine()
    End Sub
End Class

 

The Boiler class:

using System;
using OpcLabs.BaseLib.LiveMapping;
using OpcLabs.EasyOpc.UA;
using OpcLabs.EasyOpc.UA.LiveMapping;

namespace UAConsoleLiveMapping
{
    // The Boiler and its constituents are described in our application domain terms, the way we want to work with them.
    // Attributes are used to describe the correspondence between our types and members, and OPC nodes.

    // This is how the boiler looks in OPC address space:
    //  - Boiler #1
    //      - CC1001                    (CustomController)
    //          - ControlOut
    //          - Description
    //          - Input1
    //          - Input2
    //          - Input3
    //      - Drum1001                  (BoilerDrum)
    //          - LIX001                (LevelIndicator)
    //              - Output
    //      - FC1001                    (FlowController)
    //          - ControlOut
    //          - Measurement
    //          - SetPoint
    //      - LC1001                    (LevelController)
    //          - ControlOut
    //          - Measurement
    //          - SetPoint
    //      - Pipe1001                  (BoilerInputPipe)
    //          - FTX001                (FlowTransmitter)
    //              - Output
    //      - Pipe1002                  (BoilerOutputPipe)
    //          - FTX002                (FlowTransmitter)
    //              - Output

    [UANamespace("http://opcfoundation.org/UA/Boiler/")]
    [UAType]
    class Boiler
    {
        // Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

        [UANode(BrowsePath = "/PipeX001")]
        public BoilerInputPipe InputPipe = new BoilerInputPipe();

        [UANode(BrowsePath = "/DrumX001")]
        public BoilerDrum Drum = new BoilerDrum();

        [UANode(BrowsePath = "/PipeX002")]
        public BoilerOutputPipe OutputPipe = new BoilerOutputPipe();

        [UANode(BrowsePath = "/FCX001")]
        public FlowController FlowController = new FlowController();

        [UANode(BrowsePath = "/LCX001")]
        public LevelController LevelController = new LevelController();

        [UANode(BrowsePath = "/CCX001")]
        public CustomController CustomController = new CustomController();
    }

    [UAType]
    class BoilerInputPipe
    {
        // Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

        [UANode(BrowsePath = "/FTX001")]
        public FlowTransmitter FlowTransmitter1 = new FlowTransmitter();

        [UANode(BrowsePath = "/ValveX001")]
        public Valve Valve = new Valve();
    }

    [UAType]
    class BoilerDrum
    {
        // Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

        [UANode(BrowsePath = "/LIX001")]
        public LevelIndicator LevelIndicator = new LevelIndicator();
    }

    [UAType]
    class BoilerOutputPipe
    {
        // Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

        [UANode(BrowsePath = "/FTX002")]
        public FlowTransmitter FlowTransmitter2 = new FlowTransmitter();
    }

    [UAType]
    class FlowController : GenericController
    {
    }

    [UAType]
    class LevelController : GenericController
    {
    }

    [UAType]
    class CustomController
    {
        [UANode, UAData(Operations = UADataMappingOperations.Write)]    // not readable
        public double Input1 { get; set; }

        [UANode, UAData(Operations = UADataMappingOperations.Write)]    // not readable
        public double Input2 { get; set; }

        [UANode, UAData(Operations = UADataMappingOperations.Write)]    // not readable
        public double Input3 { get; set; }

        [UANode, UAData(Operations = UADataMappingOperations.ReadAndSubscribe)] // no OPC writing
        public double ControlOut { get; set; }

        [UANode, UAData]
        public string Description { get; set; }
    }

    [UAType]
    class FlowTransmitter : GenericSensor
    {
    }

    [UAType]
    class Valve : GenericActuator
    {
    }

    [UAType]
    class LevelIndicator : GenericSensor
    {
    }

    [UAType]
    class GenericController
    {
        [UANode, UAData(Operations = UADataMappingOperations.ReadAndSubscribe)] // no OPC writing
        public double Measurement { get; set; }

        [UANode, UAData]
        public double SetPoint { get; set; }

        [UANode, UAData(Operations = UADataMappingOperations.ReadAndSubscribe)] // no OPC writing
        public double ControlOut { get; set; }
    }

    [UAType]
    class GenericSensor
    {
        // Meta-members are filled in by information collected during mapping, and allow access to it later from your code.
        // Alternatively, you can derive your class from UAMappedNode, which will bring in many meta-members automatically.
        [MetaMember("NodeDescriptor")]
        public UANodeDescriptor NodeDescriptor { get; set; }

        [UANode, UAData(Operations = UADataMappingOperations.ReadAndSubscribe)] // no OPC writing
        public double Output
        {
            get => _output;
            set
            {
                _output = value;
                Console.WriteLine($"Sensor \"{NodeDescriptor}\" output is now {value}.");
            }
        }

        private double _output;
    }

    [UAType]
    class GenericActuator
    {
        [UANode, UAData(Operations = UADataMappingOperations.Write)]    // generic actuator input is not readable
        public double Input { get; set; }
    }
}
Imports OpcLabs.BaseLib.LiveMapping
Imports OpcLabs.EasyOpc.UA
Imports OpcLabs.EasyOpc.UA.LiveMapping


' The Boiler and its constituents are described in our application domain terms, the way we want to work with them.
' Attributes are used to describe the correspondence between our types and members, and OPC nodes.

' This is how the boiler looks in OPC address space:
'  - Boiler #1
'      - CC1001                    (CustomController)
'          - ControlOut
'          - Description
'          - Input1
'          - Input2
'          - Input3
'      - Drum1001                  (BoilerDrum)
'          - LIX001                (LevelIndicator)
'              - Output
'      - FC1001                    (FlowController)
'          - ControlOut
'          - Measurement
'          - SetPoint
'      - LC1001                    (LevelController)
'          - ControlOut
'          - Measurement
'          - SetPoint
'      - Pipe1001                  (BoilerInputPipe)
'          - FTX001                (FlowTransmitter)
'              - Output
'      - Pipe1002                  (BoilerOutputPipe)
'          - FTX002                (FlowTransmitter)
'              - Output

<UANamespace("http://opcfoundation.org/UA/Boiler/"), UAType()> _
Friend Class Boiler
    ' Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

    <UANode(BrowsePath:="/PipeX001")> _
    Public InputPipe As New BoilerInputPipe()

    <UANode(BrowsePath:="/DrumX001")> _
    Public Drum As New BoilerDrum()

    <UANode(BrowsePath:="/PipeX002")> _
    Public OutputPipe As New BoilerOutputPipe()

    <UANode(BrowsePath:="/FCX001")> _
    Public FlowController As New FlowController()

    <UANode(BrowsePath:="/LCX001")> _
    Public LevelController As New LevelController()

    <UANode(BrowsePath:="/CCX001")> _
    Public CustomController As New CustomController()
End Class

<UAType()> _
Friend Class BoilerInputPipe
    ' Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

    <UANode(BrowsePath:="/FTX001")> _
    Public FlowTransmitter1 As New FlowTransmitter()

    <UANode(BrowsePath:="/ValveX001")> _
    Public Valve As New Valve()
End Class

<UAType()> _
Friend Class BoilerDrum
    ' Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

    <UANode(BrowsePath:="/LIX001")> _
    Public LevelIndicator As New LevelIndicator()
End Class

<UAType()> _
Friend Class BoilerOutputPipe
    ' Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.

    <UANode(BrowsePath:="/FTX002")> _
    Public FlowTransmitter2 As New FlowTransmitter()
End Class

<UAType()> _
Friend Class FlowController
    Inherits GenericController

End Class

<UAType()> _
Friend Class LevelController
    Inherits GenericController

End Class

<UAType()> _
Friend Class CustomController
    <UANode(), UAData(Operations:=UADataMappingOperations.Write)>
    Public Property Input1 As Double

    <UANode(), UAData(Operations:=UADataMappingOperations.Write)>
    Public Property Input2 As Double

    <UANode(), UAData(Operations:=UADataMappingOperations.Write)>
    Public Property Input3 As Double

    <UANode(), UAData(Operations:=UADataMappingOperations.ReadAndSubscribe)>
    Public Property ControlOut As Double

    <UANode(), UAData()>
    Public Property Description As String
End Class

<UAType()> _
Friend Class FlowTransmitter
    Inherits GenericSensor

End Class

<UAType()> _
Friend Class Valve
    Inherits GenericActuator

End Class

<UAType()> _
Friend Class LevelIndicator
    Inherits GenericSensor

End Class

<UAType()> _
Friend Class GenericController
    <UANode(), UAData(Operations := UADataMappingOperations.ReadAndSubscribe)>
    Public Property Measurement As Double

    <UANode(), UAData()>
    Public Property SetPoint As Double

    <UANode(), UAData(Operations:=UADataMappingOperations.ReadAndSubscribe)>
    Public Property ControlOut As Double
End Class

<UAType()> _
Friend Class GenericSensor
    ' Meta-members are filled in by information collected during mapping, and allow access to it later from your code.
    ' Alternatively, you can derive your class from UAMappedNode, which will bring in many meta-members automatically.
    <MetaMember("NodeDescriptor")>
    Public Property NodeDescriptor As UANodeDescriptor

    <UANode(), UAData(Operations:=UADataMappingOperations.ReadAndSubscribe)>
    Public Property Output() As Double ' no OPC writing
        Get
            Return _output
        End Get
        Set(ByVal value As Double)
            _output = value
            Console.WriteLine("Sensor ""{0}"" output is now {1}.", NodeDescriptor, value)
        End Set
    End Property

    Private _output As Double
End Class

<UAType()> _
Friend Class GenericActuator
    <UANode(), UAData(Operations := UADataMappingOperations.Write)>
    Public Property Input As Double
End Class

 

See Also

Conceptual